
package com.directthought.elasticweb.client;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Second;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;

import javax.swing.ButtonGroup;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

import org.jets3t.service.S3ServiceException;
import org.jets3t.service.impl.rest.httpclient.RestS3Service;
import org.jets3t.service.model.S3Bucket;
import org.jets3t.service.model.S3Object;
import org.jets3t.service.security.AWSCredentials;

import com.xerox.amazonws.ec2.Jec2;
import com.xerox.amazonws.ec2.EC2Exception;
import com.xerox.amazonws.ec2.ReservationDescription;
import com.xerox.amazonws.ec2.ReservationDescription.Instance;

/**
 * This class creates the basic frame and parses command line params.
 * 
 * @author <a href="mailto:dak@directThought.com">David Kavanagh</a>
 */
public class ElasticWebControls extends JPanel implements ActionListener {
	private static Logger logger = Logger.getLogger(ElasticWebControls.class);

	private static final String START_STOP_ACTION = "startstop";
	private static final String PREFS_ACTION = "prefs";
	private static final String EXIT_ACTION = "exit";
	private static Insets margin = new Insets(0, 1, 0, 1);

	private static final String NETTICA_USER = "nettica.username";
	private static final String NETTICA_PASS = "nettica.password";
	private static final String EW_DOMAIN = "ew.domain";
	private static final String EW_HOST = "ew.host";
	private static final String EW_MIN_SERVERS = "ew.min.servers";
	private static final String EW_CAPACITY_HDRM = "ew.capacity.headroom";
	private static final String EW_AMI = "ew.ami";
	private static final String EW_KEYPAIR = "ew.keypair";
	private static final String EW_NET_GROUP = "ew.net.group";
	private static final String EW_MOCK_HTTP = "ew.enable.mock.http";

	private JFrame parent;
	private JButton startStop;
	private JTextField bucketName;
	private JList events;
	private TimeSeries data;

	private EWPreferencesModel model;
	private RestS3Service s3;
	private Map<String, Server> servers;

	public ElasticWebControls(JFrame parent) {
		this.parent = parent;
		try {
			this.model = populateModel();
		} catch (S3ServiceException ex) {
			logger.error("Problem loading preferences", ex);
		} catch (IOException ex) {
			logger.error("Problem loading preferences", ex);
		}
		buildUI();
	}

	private void buildUI() {
		setLayout(new GridBagLayout());
		GridBagConstraints gbc = new GridBagConstraints();

		gbc = new GridBagConstraints();
		gbc.anchor = GridBagConstraints.EAST;
		add(new JLabel("Bucket:"), gbc);
		bucketName = new JTextField(40);
		bucketName.setText(model.bucketName);
		bucketName.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					model.bucketName = bucketName.getText();
				}
			});
		gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.HORIZONTAL;
		gbc.gridwidth = GridBagConstraints.REMAINDER;
		gbc.weightx = 1.0;
		add(bucketName, gbc);

		data = new TimeSeries("Servers", Second.class);
		data.setMaximumItemAge(3600);
		JFreeChart chart = ChartFactory.createTimeSeriesChart("Servers Running",
							"", "", new TimeSeriesCollection(data), false, true, false);
		XYPlot plot = (XYPlot)chart.getPlot();
		plot.setBackgroundPaint(Color.black);
		ValueAxis axis = plot.getRangeAxis();
		if (axis instanceof NumberAxis) {
			NumberTickUnit ntu = ((NumberAxis)axis).getTickUnit();
			((NumberAxis)axis).setTickUnit(new NumberTickUnit(ntu.getSize(), NumberFormat.getIntegerInstance()));
		}
		JPanel graphPan = new JPanel(new BorderLayout());
		ChartPanel cp = new ChartPanel(chart);
		cp.setPreferredSize(new Dimension(300, 250));
		graphPan.add(cp, BorderLayout.CENTER);

		JPanel scalePanel = new JPanel();
		JRadioButton minutes = new JRadioButton("last 15 minutes");
		minutes.setSelected(true);
		minutes.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					data.setMaximumItemAge(900);
				}
			});
//		minutes.setMargin(margin);
		scalePanel.add(minutes);
		JRadioButton hours = new JRadioButton("last hour");
		hours.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					data.setMaximumItemAge(3600);
				}
			});
//		hours.setMargin(margin);
		scalePanel.add(hours);
		JRadioButton days = new JRadioButton("last 6 hours");
		days.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					data.setMaximumItemAge(21600);
				}
			});
//		days.setMargin(margin);
		scalePanel.add(days);
		graphPan.add(scalePanel, BorderLayout.SOUTH);
		ButtonGroup bg = new ButtonGroup();
		bg.add(minutes);
		bg.add(hours);
		bg.add(days);

        gbc = new GridBagConstraints();
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        add(new JSeparator(JSeparator.HORIZONTAL), gbc);

		JPanel eventPan = new JPanel(new GridBagLayout());
		gbc = new GridBagConstraints();
		gbc.anchor = GridBagConstraints.WEST;
		gbc.gridwidth = GridBagConstraints.REMAINDER;
		eventPan.add(new JLabel("Events:"), gbc);
		events = new JList(new DefaultListModel());
		gbc = new GridBagConstraints();
		gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.BOTH;
		gbc.weightx = 1.0;
		gbc.weighty = 1.0;
		JScrollPane pane = new JScrollPane(events);
		eventPan.add(pane, gbc);

		JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, graphPan, eventPan);
		gbc = new GridBagConstraints();
		gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.BOTH;
		gbc.weightx = 1.0;
		gbc.weighty = 2.0;
		add(split, gbc);

		startStop = new JButton("Start");
		startStop.setActionCommand(START_STOP_ACTION);
		startStop.addActionListener(this);
		gbc = new GridBagConstraints();
		gbc.anchor = GridBagConstraints.WEST;
		add(startStop, gbc);

		JButton prefs = new JButton("Preferences");
		prefs.setActionCommand(PREFS_ACTION);
		prefs.addActionListener(this);
		gbc.anchor = GridBagConstraints.EAST;
		gbc.weightx = 1.0;
		add(prefs, gbc);

		JButton exit = new JButton("Exit");
		exit.setActionCommand(EXIT_ACTION);
		exit.addActionListener(this);
		gbc.anchor = GridBagConstraints.EAST;
		gbc.gridwidth = GridBagConstraints.REMAINDER;
		add(exit, gbc);
	}
	
	public void actionPerformed(ActionEvent evt) {
		if (evt.getActionCommand().equals(START_STOP_ACTION)) {
			if (servers != null && servers.keySet().size() > 0) {
				int result = JOptionPane.showConfirmDialog(parent,
							new JLabel("Are you sure you want to shutdown the servers?"),
							"Select an option", JOptionPane.YES_NO_OPTION);
				if (result == JOptionPane.YES_OPTION) {
					// stop servers
					try {
						stopSystem();
						postEvent("stopping servers");
						startStop.setText("Start");
					} catch (S3ServiceException ex) {
						logger.error("some kind of problem stopping the servers", ex);
					}
				}
			}
			else {
				// start servers
				try {
					startSystem();
					postEvent("starting servers");
					startStop.setText("Stop");
				} catch (EC2Exception ex) {
					logger.error("some kind of problem starting the servers", ex);
				}
			}
		}
		else if (evt.getActionCommand().equals(PREFS_ACTION)) {
			final EWPreferencesDialog dialog = new EWPreferencesDialog(parent, model);
			dialog.setActionListener(new ActionListener() {
					public void actionPerformed(ActionEvent evt) {
						dialog.setVisible(false);
						UserPrefs prefs = UserPrefs.getInstance();
						try {
							s3 = new RestS3Service(new AWSCredentials(prefs.getAccessId(),
																	prefs.getSecretKey()));
						} catch (S3ServiceException ex) {
							logger.error("some kind of problem", ex);
						}
						try {
							saveModel(model);
						} catch (S3ServiceException ex) {
							logger.error("Problem saving preferences", ex);
						} catch (IOException ex) {
							logger.error("Problem saving preferences", ex);
						}
					}
				});
			dialog.setVisible(true);
		}
		else if (evt.getActionCommand().equals(EXIT_ACTION)) {
			cleanup();
			System.exit(0);
		}
	}

	private void postEvent(final String evt) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
				((DefaultListModel)events.getModel()).addElement(df.format(new Date())+" - "+evt+"\n");
				events.ensureIndexIsVisible(events.getModel().getSize()-1);
			}
		});
	}

	public void mainLoop() {
		if (isSystemRunning()) {
			startStop.setText("Stop");
		}
		else {
			startStop.setText("Start");
		}
		try {
			servers = getServerMap();
		} catch (S3ServiceException ex) {
			logger.error("Problem getting server map", ex);
		} catch (IOException ex) {
			logger.error("Problem getting server map", ex);
		}
		if (servers.keySet().size() > 0) {
			postEvent(servers.keySet().size()+" servers already running");
		}
		while (true) {
			try {
				Map<String, Server> newMap = getServerMap();
				// compare list with old list, to generate events.
				String [] oldKeys = servers.keySet().toArray(new String [] {});
				String [] newKeys = newMap.keySet().toArray(new String [] {});
				for (int i=0, k=0; i<oldKeys.length || k<newKeys.length ;) {
					//logger.info("i,k "+i+","+k);
					if (i==oldKeys.length) { // more items on new list
						if (i==0 || (i>0 && !oldKeys[i-1].equals(newKeys[k]))) {
							postEvent("server started : "+newKeys[k]);
							k++;
						}
					}
					else if (k==newKeys.length) { // more items on old list
						if (k==0 || (k>0 && !newKeys[k-1].equals(oldKeys[i]))) {
							postEvent("server terminated : "+oldKeys[i]);
							i++;
						}
					}
					else if (oldKeys[i].equals(newKeys[k])) {
						//servers.get(oldKeys[i]).equals(newMap.get(newKeys[k]))) {
						// entries are same, move on to next
						i++; k++;
					}
					else if (!newMap.keySet().contains(oldKeys[i])) {
						// old key, not in new set; server terminate`
						postEvent("server terminated : "+oldKeys[i]);
						i++;
					}
					else if (!servers.keySet().contains(newKeys[k])) {
						// new key, not in old set; server started
						postEvent("server started : "+newKeys[k]);
						k++;
					}
				}
				// look for new failures
				for (String key : newMap.keySet()) {
					Server sN = newMap.get(key);
					Server sO = servers.get(key);
					if (sN.failed && (sO == null || !sO.failed)) {
						postEvent("server failed : "+sN.instanceId);
					}
				}
				servers = newMap;
				data.add(new Second(new Date()), servers.keySet().size());
			} catch (S3ServiceException ex) {
				logger.error("Problem getting server map", ex);
			} catch (IOException ex) {
				logger.error("Problem getting server map", ex);
			}
			try { Thread.sleep(model.refreshRate); } catch (InterruptedException ex) {}
		}
	}

	public void cleanup() {
		try {
			saveModel(this.model);
		} catch (S3ServiceException ex) {
			logger.error("Problem saving preferences", ex);
		} catch (IOException ex) {
			logger.error("Problem saving preferences", ex);
		}
	}

	private void startSystem() throws EC2Exception {
		// remove shutdown file, if it exists.
		try {
			s3.deleteObject(model.bucketName, "SHUTDOWN");
		} catch (S3ServiceException ex) {
			logger.error("Problem deleting shutdown file", ex);
		}

		Jec2 ec2 = new Jec2(model.accessId, model.secretKey);
		ArrayList<String> groups = new ArrayList<String>();
		groups.add(model.group);
		String userData = model.accessId+" "+model.secretKey+" "+model.bucketName+" first";
		ec2.runInstances(model.ami, 1, 1, groups, userData, model.keypair);
	}

	private void stopSystem() throws S3ServiceException {
		S3Object obj = new S3Object("SHUTDOWN");
		obj.setContentLength(0);
		s3.putObject(model.bucketName, obj);
	}

	// call only when we need to verify if there are some of our servers running
	private boolean isSystemRunning() {
		boolean isRunning = false;
		try {
			Jec2 ec2 = new Jec2(model.accessId, model.secretKey);
			List<ReservationDescription> instances = ec2.describeInstances(new String [] {});
			for (ReservationDescription rd : instances) {
				for (Instance inst : rd.getInstances()) {
					if (inst.getState().equals("running") &&
						inst.getImageId().equals(model.ami)) {

						isRunning = true;
					}
				}
			}
		} catch (EC2Exception ex) {
			logger.error("Cannot list instances", ex);
		}
		return isRunning;
	}
	private Map<String, Server> getServerMap() throws IOException, S3ServiceException {
		S3Bucket bucket = new S3Bucket(model.bucketName);
		S3Object [] objs = s3.listObjects(bucket);
		Map<String, Server> ret = new TreeMap<String, Server>();
		for (S3Object obj : objs) {
			String key = obj.getKey();
			if (key.startsWith("IS")) {
				String instanceId = key.substring(2);
				Server s = ret.get(instanceId);
				if (s == null) s = new Server(instanceId);
				obj = s3.getObjectDetails(bucket, obj.getKey());
				s.hostname = (String)obj.getMetadata("hostname");
				s.load = (String)obj.getMetadata("load");
				ret.put(s.instanceId, s);
			}
			else if (key.startsWith("FI")) {
				String instanceId = key.substring(2);
				Server s = ret.get(instanceId);
				if (s == null) s = new Server(instanceId);
				s.failed = true;
				ret.put(s.instanceId, s);
			}
		}
		return ret;
	}

	private void saveModel(EWPreferencesModel model) throws IOException, S3ServiceException {
		// might save prefs?
		UserPrefs prefs = UserPrefs.getInstance();
		prefs.setBucket(model.bucketName);
		prefs.setRefreshRate(model.refreshRate);
		prefs.setAccessId(model.accessId);
		prefs.setSecretKey(model.secretKey);
		saveEWProperties(model);
	}

	private EWPreferencesModel populateModel() throws IOException, S3ServiceException {
		EWPreferencesModel ret;
		UserPrefs prefs = UserPrefs.getInstance();
		Properties props = loadEWProperties(prefs.getBucket());
		ret = new EWPreferencesModel(prefs.getBucket(), prefs.getRefreshRate(),
						prefs.getAccessId(), prefs.getSecretKey(),
						props.getProperty(NETTICA_USER), props.getProperty(NETTICA_PASS),
						props.getProperty(EW_DOMAIN), props.getProperty(EW_HOST),
						Integer.parseInt(props.getProperty(EW_MIN_SERVERS).trim()),
						Integer.parseInt(props.getProperty(EW_CAPACITY_HDRM).trim()),
						props.getProperty(EW_AMI),
						props.getProperty(EW_KEYPAIR), props.getProperty(EW_NET_GROUP),
						Boolean.parseBoolean(props.getProperty(EW_MOCK_HTTP)));
		return ret;
	}

	private Properties loadEWProperties(String bucket) throws IOException, S3ServiceException {
		UserPrefs prefs = UserPrefs.getInstance();
		s3 = new RestS3Service(new AWSCredentials(prefs.getAccessId(), prefs.getSecretKey()));
		Properties ret = new Properties();
		S3Object obj = s3.getObject(bucket, "elasticweb.properties",
							null, null, null, null, null, null);
		ret.load(obj.getDataInputStream());
		return ret;
	}

	private void saveEWProperties(EWPreferencesModel model) throws IOException, S3ServiceException {
		Properties props = new Properties();
		props.setProperty(NETTICA_USER, model.netticaUsername);
		props.setProperty(NETTICA_PASS, model.netticaPassword);
		props.setProperty(EW_DOMAIN, model.domainName);
		props.setProperty(EW_HOST, model.hostName);
		props.setProperty(EW_MIN_SERVERS, ""+model.minServers);
		props.setProperty(EW_CAPACITY_HDRM, ""+model.headroom);
		props.setProperty(EW_AMI, model.ami);
		props.setProperty(EW_KEYPAIR, model.keypair);
		props.setProperty(EW_NET_GROUP, model.group);
		props.setProperty(EW_MOCK_HTTP, model.runMockHttp?"true":"false");
		File tmpFile = File.createTempFile("eweb", "tmp", new File("."));
		props.store(new FileOutputStream(tmpFile), "written by ElasticWeb Monitor");
		S3Object obj = new S3Object("elasticweb.properties");
		obj.setDataInputFile(tmpFile);
		obj.setContentLength(tmpFile.length());
		s3.putObject(model.bucketName, obj);
		tmpFile.deleteOnExit();
	}

	class Server {
		public String instanceId;
		public String hostname;
		public String load;
		public boolean failed;

		Server(String instanceId) {
			this.instanceId = instanceId;
		}

		public boolean equals(Object obj) {
			Server s = (Server)obj;
			return (instanceId.equals(s.instanceId) &&
					hostname.equals(s.hostname) &&
					load.equals(s.load) &&
					failed == s.failed);
		}
	}
}
